Skip to content

feat: initializeApp idempotency #2947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open

Conversation

DellaBitta
Copy link

@DellaBitta DellaBitta commented Jul 2, 2025

Discussion

Update initializeApp to return an existing app if one exists with the same name and the same AppOptions. However, due to their inabilty to be compared, initializeApp will throw if an existing app has a configured Credential or httpAgent.

Additionally adds reference docs to getApp and getApps.

Testing

Updated unit tests.

API Changes

Internal: go/fb-admin-init-idempotency

Fixes: #2111

@DellaBitta DellaBitta marked this pull request as ready for review July 10, 2025 15:07
@DellaBitta DellaBitta requested a review from lahirumaramba July 10, 2025 15:07
@DellaBitta DellaBitta changed the title initializeApp idempotency (WIP candidate) initializeApp idempotency Jul 10, 2025
@lahirumaramba lahirumaramba self-assigned this Jul 14, 2025
@lahirumaramba lahirumaramba added release-note release:stage Stage a release candidate labels Jul 14, 2025
@lahirumaramba lahirumaramba changed the title initializeApp idempotency feat: initializeApp idempotency Jul 15, 2025
Copy link
Member

@lahirumaramba lahirumaramba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM! Added a few comments. Thank you!

* or `options.credential` are defined. This is due to the function's inability to
* determine if the existing {@link App}'s `options` compare to the `options` parameter
* of this function. It's recommended to use {@link getApp} or {@link getApps} if your
* implementation uses either of these two fields in {@link AppOptions}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we extend the JSDoc with an example?

/**...
 * For example, to safely initialize an app that may already exist:
 *
 * ```javascript
 * let app;
 * try {
 * app = getApp("myApp");
 * } catch (error) {
 * app = initializeApp({ credential: myCredential }, "myApp");
 * }
 * ```
 */

} else if (autoInit) {
// Auto-initialization is triggered when no options were passed to
// initializeApp. With no options to compare, simply return the App.
return currentApp;
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify this by using early exits/guard clauses (and removing the nested if else statements)?
If it doesn't work in this case feel free to ignore. For example:

if (this.appStore.has(appName)) {
  const currentApp = this.appStore.get(appName)!;

  if (currentApp.autoInit() !== autoInit) {
    throw new FirebaseAppError(
      AppErrorCodes.INVALID_APP_OPTIONS,
      `Firebase app named "${appName}" attempted mismatch between custom AppOptions` +
      ' and an App created via Auto Init.'
    )
  }

  if (autoInit) {
    return currentApp;
  }

if (typeof options.httpAgent !== 'undefined') {
.....


// `httpAgent` objects cannot be compared with deepEquals impls.
// Idempotency cannot be supported if one exists.
if (typeof options.httpAgent !== 'undefined') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it make sense to move this (and below) validation to a separate function (out of initializeApp) to clean it up a bit?

* @internal
*/
function validateAppNameFormat(appName: string): void {
if (typeof appName !== 'string' || appName === '') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use validator.isNonEmptyString() here?

export function isNonEmptyString(value: any): value is string {

export function initializeApp(options?: AppOptions, appName: string = DEFAULT_APP_NAME): App {
return defaultAppStore.initializeApp(options, appName);
}

/**
* Returns an existing App instance for the provided name. If no name is provided
* the the default app name is queried.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queried or used?

* A (read-only) array of all initialized apps.
*
* @returns An array containing all initialized apps.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get @egilmorez's review on the new doc changes, please. Thanks!

'name.',
);
AppErrorCodes.INVALID_APP_OPTIONS,
`Firebase app named "${appName}" attempted mismatch between custom AppOptions` +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify this error message? I am not sure Auto Init is a well known term in the Admin SDK world

Copy link
Contributor

@egilmorez egilmorez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks David!

Some things to look at, most particularly literals. Professor Gilmorez urges you to review https://docs.google.com/presentation/d/1Ezb4sp4XD6ItHISccnME-EVH_oJj59R5Naq0IskLrAo/edit?slide=id.g1cd7eabb964_0_10#slide=id.g1cd7eabb964_0_10.


/**
* Returns `true` if the FirebaseApp instance was initialized with a custom
* Credential.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd lowercase "credential" unless it's a literal.

And I would backtick FirebaseApp as a literal.

options = loadOptionsFromEnvVar();
options.credential = getApplicationDefault();
}

if (typeof appName !== 'string' || appName === '') {
// Check if an App already exists and, if so, ensure its AppOptions match
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of literals here too: "App already exists and, if so, ensure its AppOptions"

But maybe these comments don't get parsed for public docs; in that case, no big deal :)

* Validates that the `requestedOptions` and the `existingApp` options objects
* do not have fields that would break deep equals comparisons.
*
* @param requestedOptions The incoming AppOptions of a new initailizeApp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the literals in these @tags need to be backticked? I would guess so.

export const defaultAppStore = new AppStore();

/**
* Initializes the App instance.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either backtick the literal, or just call it an "app instance."

* Note, due to the inablity to compare `http.Agent` objects and `Credential` objects,
* this function cannot support idempotency if either of `options.httpAgent` or
* `options.credential` are defined. When either is defined, subsequent invocations will
* throw instead of returning an {@link App} object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be something like "throw an error," or cite the error name?

* the provided configuration.
*
* @throws FirebaseAppError if an {@link App} with the same name has already been
* initialized with a different set of AppOptions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about the literals in these tagged descriptions:

  • @param options - Optional A set of {@link AppOptions} for the {@link App} instance.
  • If not present, initializeApp will try to initialize with the options from the
  • FIREBASE_CONFIG environment variable. If the environment variable contains a string
  • that starts with { it will be parsed as JSON, otherwise it will be assumed to be
  • pointing to a file.
  • @param appName - Optional name of the FirebaseApp instance.
  • @returns A new App instance, or the existing App if the instance already exists with
  • the provided configuration.
  • @throws FirebaseAppError if an {@link App} with the same name has already been
  • initialized with a different set of AppOptions.
  • @throws FirebaseAppError

export function initializeApp(options?: AppOptions, appName: string = DEFAULT_APP_NAME): App {
return defaultAppStore.initializeApp(options, appName);
}

/**
* Returns an existing App instance for the provided name. If no name is provided
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More questions about literals and backticks:

  • @param options - Optional A set of {@link AppOptions} for the {@link App} instance.
  • If not present, initializeApp will try to initialize with the options from the
  • FIREBASE_CONFIG environment variable. If the environment variable contains a string
  • that starts with { it will be parsed as JSON, otherwise it will be assumed to be
  • pointing to a file.
  • @param appName - Optional name of the FirebaseApp instance.
  • @returns A new App instance, or the existing App if the instance already exists with
  • the provided configuration.
  • @throws FirebaseAppError if an {@link App} with the same name has already been
  • initialized with a different set of AppOptions.
  • @throws FirebaseAppError

@DellaBitta DellaBitta requested a review from egilmorez July 30, 2025 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:stage Stage a release candidate release-note
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Firebase.initializeApp throwing default Firebase app already exists errors in Next.js apps.
3 participants